package manager;

import dao.FileDao;
import entity.FileMeta;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

// 该类是用来遍历分析目录结构，对目录/文件进行扫描，并把扫描的结果同步更新到数据库中

public class FileManager {
    private FileDao fileDao = new FileDao();

    /*
    由于把扫描工作交给线程池完成，主线程只负责遍历目录，就可能遍历目录瞬间就完成了，而扫描工作还差得远
    此时直接停止计时，就不太准确，计算的时间会存在很大误差，所以使用 CountDownLatch 来解决这个问题
     */
    private CountDownLatch countDownLatch = null;
    // 用来衡量任务结束的计数器，使用 int 可能会有线程安全问题，AtomicInteger 内部是原子操作，所以不会涉及线程安全问题
    private AtomicInteger taskCount = new AtomicInteger(0);
    /**
     * 通过这个方法，实现针对 basePath 描述的目录中 内容的扫描
     * 把这里的文件和子目录都扫描清楚，并且保存在数据库中，该方法需要考虑子目录
     * @param basePath
     */
    public void scanAll(File basePath) {
        System.out.println("[FileManager] scanAll 开始！");
        long start = System.currentTimeMillis();

        // 保证每次调用 scanAll 都重新创建 CountDownLatch. 重新进行计数了.
        // 初始化设置选手数目为 1，当线程池执行完所有的任务之后，就立即调用一次 countDown 撞线
        countDownLatch = new CountDownLatch(1);

        // 测单线程和多线程扫描（分首次扫描和后续扫描）
//        scanAllByOneThread(basePath); // 单线程扫描
        scanAllByThreadPool(basePath); // 多线程的线程池进行扫描
        // 当前使用 await 让主线程等待
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long end = System.currentTimeMillis();
        System.out.println("[FileManager] scanAll 结束！执行时间：" + (end - start) + " ms");
    }

    /**
     * 实现单线程扫描一个目录 ---- 但是速度比较慢
     * @param basePath 一个目录
     */
    private void scanAllByOneThread(File basePath) {
        if(!basePath.isDirectory()) {
            return;
        }
        // 先针对当前目录进行扫描，列出这个 basePath 下包含的文件和目录，并且把这些内容更新到数据库中
        scan(basePath);

        // 列出当前目录下包含的所有文件
        File[] files = basePath.listFiles();
        if (files == null || files.length == 0) {
            // 当前目录下没有东西
            return;
        }
        for (File f : files) {
            if(f.isDirectory()) {
                scanAllByOneThread(f);
            }
        }
    }

    // 创建线程池，线程数量为 8
    // 加上 static 是为了让这个线程池成为单例的，始终只有一份线程池
    private static ExecutorService executorService = Executors.newFixedThreadPool(8);

    /**
     * 使用多线程扫描。把当前的扫描工作拆分成多个小任务，把每个 scan 都当成一个小任务，放到线程池中执行
     * @param basePath   一个目录
     */
    private void scanAllByThreadPool(File basePath) {
        if(!basePath.isDirectory()) {
            return;
        }

        // 计数器自增，每次有个任务，都自增一下
        taskCount.getAndIncrement();  // 相当于 taskCount++

        // 扫描操作，放到线程池中完成
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    scan(basePath);  // 因为如果这里的 scan 抛出异常了，就导致下边自减不能够执行，所以需要 finally
                } finally {
                    /* 计数器自减
                        把自减逻辑放到 finally 里边，确保自减操作是肯定能够执行到的
                     */
                    taskCount.getAndDecrement(); // 相当于 taskCount--
                    if(taskCount.get() == 0) {
                        // 如果计数器为 0 了，就通知主线程停表了
                        countDownLatch.countDown();
                    }
                }
            }
        });

        // 继续递归其他目录
        File[] files = basePath.listFiles();
        if (files == null || files.length == 0) {
            // 当前目录下没有东西
            return;
        }
        for (File f : files) {
            if(f.isDirectory()) {  // 如果是目录，递归使用多线程扫描
                scanAllByThreadPool(f);
            }
        }
    }

    /**
     * scan 方法只针对一个目录进行扫描处理。（整个遍历目录过程中的基本操作）
     * 这个方法只是针对当前的 path 对应的目录进行分析  * 此方法不考虑子目录里面的内容
     * 只列出这个 path 下包含的文件和子目录，并且把这些内容更新到数据库中
     * @param path
     */
    public void scan(File path) {
        // DEBUG 这个日志打印的有点多，先临时去掉
//        System.out.println("[FileManager] 扫描路径: " + path.getAbsolutePath());

        // 1. 列出文件系统上真实的文件/目录
        List<FileMeta> scanned = new ArrayList<>();
        // listFiles()方法是返回某个目录下所有文件和目录的绝对路径，返回的是File数组
        File[] files = path.listFiles();
        if(files != null) {
            for (File f : files) {
                scanned.add(new FileMeta(f));  // 将每一个文件/目录都加入到 scanned 中
            }
        }

        // 2. 列出数据库里当前指定目录里面的内容
        List<FileMeta> saved = fileDao.searchByPath(path.getPath());

        // 3. 找出文件系统中没有，数据库中有的，把这些内容从数据库中删除掉
        List<FileMeta> forDelete = new ArrayList<>();
        for(FileMeta fileMeta : saved) {
            if(!scanned.contains(fileMeta)) {
                forDelete.add(fileMeta);
            }
        }
        fileDao.delete(forDelete);

        // 4. 找出文件系统中有的，数据库中没有的，把这些内容添加到数据库中
        List<FileMeta> forAdd = new ArrayList<>();
        for(FileMeta fileMeta : scanned) {
            if(!saved.contains(fileMeta)) { // 找出文件系统中有的，数据库中没有的
                forAdd.add(fileMeta);
            }
        }
        fileDao.add(forAdd);
    }
}
